## Copyright 2015-2019 Ilgar Lunin, Pedro Cabrera

## Licensed under the Apache License, Version 2.0 (the "License");
## you may not use this file except in compliance with the License.
## You may obtain a copy of the License at

##     http://www.apache.org/licenses/LICENSE-2.0

## Unless required by applicable law or agreed to in writing, software
## distributed under the License is distributed on an "AS IS" BASIS,
## WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
## See the License for the specific language governing permissions and
## limitations under the License.


"""
.. sidebar:: **Common.py**

    **Common.py** is a common definitions file. This file is imported in almost all others files of the program

"""

import re
import math
import time
import struct
import weakref
import sys

from enum import IntEnum, Flag, auto

from PyFlow import findPinClassByType
from PyFlow.Core.version import Version
from PyFlow import GET_PACKAGES

maxint = 2 ** (struct.Struct("i").size * 8 - 1) - 1


FLOAT_RANGE_MIN = 0.1 + (-maxint - 1.0)
FLOAT_RANGE_MAX = maxint + 0.1
INT_RANGE_MIN = -maxint + 0
INT_RANGE_MAX = maxint + 0

DEFAULT_IN_EXEC_NAME = "inExec"
DEFAULT_OUT_EXEC_NAME = "outExec"
DEFAULT_WIDGET_VARIANT = "DefaultWidget"
REF = "Reference"

global GlobalVariables
GlobalVariables = {}


def fetchPackageNames(graphJson):
    """Parses serialized graph and returns all package names it uses

    :param graphJson: Serialized graph
    :type graphJson: dict
    :rtyoe: list(str)
    """
    packages = set()

    def worker(graphData):
        for node in graphData["nodes"]:
            packages.add(node["package"])

            for inpJson in node["inputs"]:
                packages.add(inpJson["package"])

            for outJson in node["inputs"]:
                packages.add(outJson["package"])

            if "graphData" in node:
                worker(node["graphData"])

    worker(graphJson)
    return packages


def validateGraphDataPackages(graphData, missedPackages=None):
    """Checks if packages used in serialized data accessible

    Missed packages will be added to output set

    :param graphData: Serialized graph
    :type graphData: dict
    :param missedPackages: Package names that missed
    :type missedPackages: set
    :rtype: bool
    """
    if missedPackages is None:
        missedPackages = set()
    existingPackages = GET_PACKAGES().keys()
    graphPackages = fetchPackageNames(graphData)
    for pkg in graphPackages:
        if pkg not in existingPackages:
            missedPackages.add(pkg)
    return len(missedPackages) == 0

def lerp(start, end, alpha):
    """Performs a linear interpolation

    >>> start + alpha * (end - start)

    :param start: start the value to interpolate from
    :param end: end the value to interpolate to
    :param alpha: alpha how far to interpolate
    :returns: The result of the linear interpolation
    """
    return start + alpha * (end - start)


def GetRangePct(MinValue, MaxValue, Value):
    """Calculates the percentage along a line from **MinValue** to **MaxValue** that value is.

    :param MinValue: Minimum Value
    :param MaxValue: Maximum Value
    :param Value: Input value
    :returns: The percentage (from 0.0 to 1.0) between the two values where input value is
    """
    return (Value - MinValue) / (MaxValue - MinValue)


def mapRangeClamped(Value, InRangeA, InRangeB, OutRangeA, OutRangeB):
    """Returns Value mapped from one range into another where the Value is clamped to the Input Range.
    (e.g. 0.5 normalized from the range 0->1 to 0->50 would result in 25)
    """

    ClampedPct = clamp(GetRangePct(InRangeA, InRangeB, Value), 0.0, 1.0)
    return lerp(OutRangeA, OutRangeB, ClampedPct)


def mapRangeUnclamped(Value, InRangeA, InRangeB, OutRangeA, OutRangeB):
    """Returns Value mapped from one range into another where the Value is clamped to the Input Range.
    (e.g. 0.5 normalized from the range 0->1 to 0->50 would result in 25)"""
    return lerp(OutRangeA, OutRangeB, GetRangePct(InRangeA, InRangeB, Value))


def sign(x):
    """Returns sign of x. -1 if x is negative, 1 if positive and zero if 0.

    >>> x and (1, -1)[x < 0]
    """
    return x and (1, -1)[x < 0]


def currentProcessorTime():
    return time.process_time()


def clamp(n, vmin, vmax):
    """Computes the value of the first specified argument clamped to a range defined by the second and third specified arguments

    :param n: input Value
    :param vmin: MiniMum Value
    :param vmax: Maximum Value
    :returns: The clamped value of n
    """
    return max(min(n, vmax), vmin)


def roundup(x, to):
    """Rounding up to sertain value

    >>> roundup(7, 8)
    >>> 8
    >>> roundup(8, 8)
    >>> 8
    >>> roundup(9, 8)
    >>> 16

    :param x: value to round
    :param to: value x will be rounded to
    :returns: rounded value of x
    :rtype: int
    """
    return int(math.ceil(x / to)) * to


_currentVersion = Version(sys.version_info.major, sys.version_info.minor, 0)
python32 = Version(3, 2, 0)
if _currentVersion <= python32:

    def clearList(list):
        """Clears python list

        :param list:  list to clear
        :type list: list
        :returns: cleared List
        :rtype: list
        """
        del list[:]


else:

    def clearList(list):
        """Clears python list

        :param list:  list to clear
        :type list: list
        :returns: cleared List
        :rtype: list
        """
        list.clear()


def findGoodId(ids):
    """
    Finds good minimum unique int from iterable. Starting from 1

    :param ids: a collection of occupied ids
    :type ids: list|set|tuple
    :returns: Unique Id
    :rtype: int
    """
    if len(ids) == 0:
        return 1

    ids = sorted(set(ids))
    lastID = min(ids)

    if lastID > 1:
        return 1

    for ID in ids:
        diff = ID - lastID
        if diff > 1:
            return lastID + 1
        lastID = ID
    else:
        return ID + 1


def wrapStringToFunctionDef(functionName, scriptString, kwargs=None):
    """Generates function string which then can be compiled and executed

    Example:
    ::

        wrapStringToFunctionDef('test', 'print(a)', {'a': 5})

    Will produce following function:
    ::

        def test(a=5):
            print(a)
    """
    kwargsString = ""
    if kwargs is not None:
        for argname, argValue in kwargs.items():
            if isinstance(argValue, str):
                argValue = "'{}'".format(argValue)
            kwargsString += "{0}={1}, ".format(argname, argValue)
        kwargsString = kwargsString[:-2]

    result = "def {0}({1}):\n".format(functionName, kwargsString)

    for scriptLine in scriptString.split("\n"):
        result += "\t{}".format(scriptLine)
        result += "\n"
    return result


def cycleCheck(src, dst):
    """Check for cycle connected nodes

    :param src: hand side pin
    :type src: :class:`PyFlow.Core.PinBase.PinBase`
    :param dst: hand side pin
    :type dst: :class:`PyFlow.Core.PinBase.PinBase`
    :returns: True if cycle deleted
    :rtype: bool
    """
    if src.direction == PinDirection.Input:
        src, dst = dst, src
    start = src
    if src in dst.affects:
        return True
    for i in dst.affects:
        if cycleCheck(start, i):
            return True
    return False


def arePinsConnected(src, dst):
    """Checks if two pins are connected

    .. note:: Pins can be passed in any order if **src** pin is :py:class:`PyFlow.Core.Common.PinDirection`, they will be swapped

    :param src: left hand side pin
    :type src: :py:class:`PyFlow.Core.PinBase.PinBase`
    :param dst: right hand side pin
    :type dst: :py:class:`PyFlow.Core.PinBase.PinBase`
    :returns: True if Pins are connected
    :rtype: bool
    """
    if src.direction == dst.direction:
        return False
    if src.owningNode() == dst.owningNode():
        return False
    if src.direction == PinDirection.Input:
        src, dst = dst, src
    if dst in src.affects and src in dst.affected_by:
        return True
    return False


def getConnectedPins(pin):
    """Find all connected Pins to input Pin

    :param pin: Pin to search connected pins
    :type pin: :py:class:`PyFlow.Core.PinBase.PinBase`
    :returns: Set of connected pins
    :rtype: set(:py:class:`PyFlow.Core.PinBase.PinBase`)
    """
    result = set()
    if pin.direction == PinDirection.Input:
        for lhsPin in pin.affected_by:
            result.add(lhsPin)
    if pin.direction == PinDirection.Output:
        for rhsPin in pin.affects:
            result.add(rhsPin)
    return result


def pinAffects(lhs, rhs):
    """This function for establish dependencies between pins

    .. warning:: Used internally, users will hardly need this

    :param lhs: First pin to connect
    :type lhs: :py:class:`PyFlow.Core.PinBase.PinBase`
    :param rhs: Second Pin to connect
    :type rhs: :py:class:`PyFlow.Core.PinBase.PinBase`
    """
    assert lhs is not rhs, "pin can not affect itself"
    lhs.affects.add(rhs)
    rhs.affected_by.add(lhs)


def canConnectPins(src, dst):
    """**Very important fundamental function, it checks if connection between two pins is possible**

    :param src: Source pin to connect
    :type src: :py:class:`PyFlow.Core.PinBase.PinBase`
    :param dst: Destination pin to connect
    :type dst: :py:class:`PyFlow.Core.PinBase.PinBase`
    :returns: True if connection can be made, and False if connection is not possible
    :rtype: bool
    """
    if src is None or dst is None:
        return False

    if src.direction == dst.direction:
        return False

    if arePinsConnected(src, dst):
        return False

    if src.direction == PinDirection.Input:
        src, dst = dst, src

    if cycleCheck(src, dst):
        return False

    if src.isExec() and dst.isExec():
        return True

    if not src.isArray() and dst.isArray():
        if dst.optionEnabled(PinOptions.SupportsOnlyArrays):
            if not src.canChangeStructure(dst._currStructure, []):
                return False

        if not dst.canChangeStructure(src._currStructure, [], selfCheck=False):
            if not src.canChangeStructure(dst._currStructure, [], selfCheck=False):
                return False

    if not src.isDict() and dst.isDict():
        if dst.optionEnabled(PinOptions.SupportsOnlyArrays):
            if not (
                src.canChangeStructure(dst._currStructure, [])
                or dst.canChangeStructure(src._currStructure, [], selfCheck=False)
            ):
                return False
        elif (
            not src.supportDictElement(
                [], src.optionEnabled(PinOptions.DictElementSupported)
            )
            and dst.optionEnabled(PinOptions.SupportsOnlyArrays)
            and not dst.canChangeStructure(src._currStructure, [], selfCheck=False)
        ):
            return False
        else:
            DictElementNode = src.getDictElementNode([])
            dictNode = dst.getDictNode([])
            nodeFree = False
            if dictNode:
                nodeFree = dictNode.KeyType.checkFree([])
            if DictElementNode:
                if not DictElementNode.key.checkFree([]) and not nodeFree:
                    if dst._data.keyType != DictElementNode.key.dataType:
                        return False

    if src.isArray() and not dst.isArray():
        srcCanChangeStruct = src.canChangeStructure(dst._currStructure, [])
        dstCanChangeStruct = dst.canChangeStructure(
            src._currStructure, [], selfCheck=False
        )
        if not dst.optionEnabled(PinOptions.ArraySupported) and not (
            srcCanChangeStruct or dstCanChangeStruct
        ):
            return False

    if src.isDict() and not dst.isDict():
        srcCanChangeStruct = src.canChangeStructure(dst._currStructure, [])
        dstCanChangeStruct = dst.canChangeStructure(
            src._currStructure, [], selfCheck=False
        )
        if not dst.optionEnabled(PinOptions.DictSupported) and not (
            srcCanChangeStruct or dstCanChangeStruct
        ):
            return False

    if dst.hasConnections():
        if (
            not dst.optionEnabled(PinOptions.AllowMultipleConnections)
            and dst.reconnectionPolicy == PinReconnectionPolicy.ForbidConnection
        ):
            return False

    if src.hasConnections():
        if (
            not src.optionEnabled(PinOptions.AllowMultipleConnections)
            and src.reconnectionPolicy == PinReconnectionPolicy.ForbidConnection
        ):
            return False

    if src.owningNode().graph() is None or dst.owningNode().graph() is None:
        return False

    if src.owningNode().graph() is not dst.owningNode().graph():
        return False

    if src.isAny() and dst.isExec():
        if src.dataType not in dst.supportedDataTypes():
            return False

    if src.isExec() and not dst.isExec():
        return False

    if not src.isExec() and dst.isExec():
        return False

    if src.IsValuePin() and dst.IsValuePin():
        if src.dataType in dst.allowedDataTypes(
            [], dst._supportedDataTypes
        ) or dst.dataType in src.allowedDataTypes([], src._supportedDataTypes):
            a = src.dataType == "AnyPin" and not src.canChangeTypeOnConnection(
                [], src.optionEnabled(PinOptions.ChangeTypeOnConnection), []
            )
            b = dst.canChangeTypeOnConnection(
                [], dst.optionEnabled(PinOptions.ChangeTypeOnConnection), []
            ) and not dst.optionEnabled(PinOptions.AllowAny)
            c = not dst.canChangeTypeOnConnection(
                [], dst.optionEnabled(PinOptions.ChangeTypeOnConnection), []
            ) and not dst.optionEnabled(PinOptions.AllowAny)
            if all([a, b or c]):
                return False
            if (
                not src.isDict()
                and dst.supportOnlyDictElement([], dst.isDict())
                and not (
                    dst.checkFree([], selfCheck=False)
                    and dst.canChangeStructure(src._currStructure, [], selfCheck=False)
                )
            ):
                if not src.supportDictElement(
                    [], src.optionEnabled(PinOptions.DictElementSupported)
                ) and dst.supportOnlyDictElement([], dst.isDict()):
                    return False
            return True
        else:
            if src.dataType not in dst.supportedDataTypes():
                return False

            if all(
                [
                    src.dataType
                    in list(
                        dst.allowedDataTypes(
                            [],
                            dst._defaultSupportedDataTypes,
                            selfCheck=dst.optionEnabled(
                                PinOptions.AllowMultipleConnections
                            ),
                            defaults=True,
                        )
                    )
                    + ["AnyPin"],
                    dst.checkFree(
                        [],
                        selfCheck=dst.optionEnabled(
                            PinOptions.AllowMultipleConnections
                        ),
                    ),
                ]
            ):
                return True
            if all(
                [
                    dst.dataType
                    in list(
                        src.allowedDataTypes(
                            [], src._defaultSupportedDataTypes, defaults=True
                        )
                    )
                    + ["AnyPin"],
                    src.checkFree([]),
                ]
            ):
                return True
        return False

    if src.owningNode == dst.owningNode:
        return False

    return True


def connectPins(src, dst):
    """**Connects two pins**

    These are the rules how pins connect:

    * Input value pins can have one output connection if :py:class:`PyFlow.Core.Common.PinOptions.AllowMultipleConnections` flag is disabled
    * Output value pins can have any number of connections
    * Input execs can have any number of connections
    * Output execs can have only one connection

    :param src: left hand side pin
    :type src: :py:class:`PyFlow.Core.PinBase.PinBase`
    :param dst: right hand side pin
    :type dst: :py:class:`PyFlow.Core.PinBase.PinBase`
    :returns: True if connected Successfully
    :rtype: bool
    """
    if src.direction == PinDirection.Input:
        src, dst = dst, src

    if not canConnectPins(src, dst):
        return False

    # input value pins can have one output connection if `AllowMultipleConnections` flag is disabled
    # output value pins can have any number of connections
    if src.IsValuePin() and dst.IsValuePin():
        if dst.hasConnections():
            if not dst.optionEnabled(PinOptions.AllowMultipleConnections):
                dst.disconnectAll()

    # input execs can have any number of connections
    # output execs can have only one connection
    if src.isExec() and dst.isExec():
        if src.hasConnections():
            if not src.optionEnabled(PinOptions.AllowMultipleConnections):
                src.disconnectAll()

    if src.isExec() and dst.isExec():
        src.onExecute.connect(dst.call)

    dst.aboutToConnect(src)
    src.aboutToConnect(dst)

    pinAffects(src, dst)
    src.setDirty()

    dst.setData(src.currentData())

    dst.pinConnected(src)
    src.pinConnected(dst)
    #push(dst)
    return True


def connectPinsByIndexes(lhsNode=None, lhsOutPinIndex=0, rhsNode=None, rhsInPinIndex=0):
    """Connects pins regardless name.

    This function uses pin locations on node. Top most pin have position index 1, pin below - 2 etc.

    :param lhsNode: Left hand side node
    :type lhsNode: :class:`~PyFlow.Core.NodeBase.NodeBase`
    :param lhsOutPinIndex: Out pin position on left hand side node
    :type lhsOutPinIndex: int
    :param rhsNode: Right hand side node
    :type rhsNode: :class:`~PyFlow.Core.NodeBase.NodeBase`
    :param rhsInPinIndex: Out pin position on right hand side node
    :type rhsInPinIndex: int
    """
    if lhsNode is None:
        return False

    if rhsNode is None:
        return False

    if lhsOutPinIndex not in lhsNode.orderedOutputs:
        return False

    if rhsInPinIndex not in rhsNode.orderedInputs:
        return False

    lhsPin = lhsNode.orderedOutputs[lhsOutPinIndex]
    rhsPin = rhsNode.orderedInputs[rhsInPinIndex]

    return connectPins(lhsPin, rhsPin)


def traverseConstrainedPins(startFrom, callback):
    """Iterate over constrained and connected pins

    Iterates over all constrained chained pins of type :class:`Any <PyFlow.Packages.PyFlowBase.Pins.AnyPin.AnyPin>` and passes pin into callback function. Callback will be executed once for every pin

    :param startFrom: First pin to start Iteration
    :type startFrom: :class:`~PyFlow.Core.PinBase.PinBase`
    :param callback: Functor to execute in each iterated pin.
    :type callback: callback(:class:`~PyFlow.Core.PinBase.PinBase`)
    """
    if not startFrom.isAny():

        return
    traversed = set()

    def worker(pin):
        traversed.add(pin)
        callback(pin)

        if pin.constraint is None:
            nodePins = set()
        else:
            nodePins = set(pin.owningNode().constraints[pin.constraint])

        for connectedPin in getConnectedPins(pin):
            if connectedPin.isAny():
                nodePins.add(connectedPin)
        for neighbor in nodePins:
            if neighbor not in traversed:
                worker(neighbor)

    worker(startFrom)


def disconnectPins(src, dst):
    """Disconnects two pins

    :param src: left hand side pin
    :type src: :py:class:`~PyFlow.Core.PinBase.PinBase`
    :param dst: right hand side pin
    :type dst: :py:class:`~PyFlow.Core.PinBase.PinBase`
    :returns: True if disconnection success
    :rtype: bool
    """
    if arePinsConnected(src, dst):
        if src.direction == PinDirection.Input:
            src, dst = dst, src
        src.affects.remove(dst)
        dst.affected_by.remove(src)
        src.pinDisconnected(dst)
        dst.pinDisconnected(src)
        push(dst)
        if src.isExec() and dst.isExec():
            src.onExecute.disconnect(dst.call)
        return True
    return False


def push(start_from):
    """Marks dirty all ports from start to the right

    this part of graph will be recomputed every tick

    :param start_from: pin from which recursion begins
    :type start_from: :py:class:`~PyFlow.Core.PinBase.PinBase`
    """
    #print("push", start_from.name, start_from.owningNode().name)
    if not len(start_from.affects) == 0:
        start_from.setDirty()
        for i in start_from.affects:
            i.setDirty()
            push(i)


def extractDigitsFromEndOfString(string):
    """Get digits at end of a string

    Example:

    >>> nums = extractDigitsFromEndOfString("h3ello154")
    >>> print(nums, type(nums))
    >>> 154 <class 'int'>

    :param string: Input numbered string
    :type string: str
    :returns: Numbers in the final of the string
    :rtype: int
    """
    result = re.search(r"(\d+)$", string)
    if result is not None:
        return int(result.group(0))


def removeDigitsFromEndOfString(string):
    """Delete the numbers at the end of a string

    Similar to :func:`~PyFlow.Core.Common.extractDigitsFromEndOfString`, but removes digits in the end.

    :param string: Input string
    :type string: string
    :returns: Modified string
    :rtype: string
    """
    return re.sub(r"\d+$", "", string)


def getUniqNameFromList(existingNames, name):
    """Create unique name

    Iterates over **existingNames** and extracts the end digits to find a new unique id

    :param existingNames: List or set of strings where to search for existing indexes
    :type existingNames: list[str]|set[str]
    :param name: Name to obtain a unique version from
    :type name: str
    :returns: New name non overlapping with any in existingNames
    :rtype: str
    """
    if name not in existingNames:
        return name
    ids = set()
    for existingName in existingNames:
        digits = extractDigitsFromEndOfString(existingName)
        if digits is not None:
            ids.add(digits)
    idx = findGoodId(ids)
    nameNoDigits = removeDigitsFromEndOfString(name)
    return nameNoDigits + str(idx)


def clearSignal(signal):
    """Disconnects all receivers

    :param signal: emitter
    :type signal: :class:`~blinker.base.Signal`
    """
    for receiver in list(signal.receivers.values()):
        if isinstance(receiver, weakref.ref):
            signal.disconnect(receiver())
        else:
            signal.disconnect(receiver)


class SingletonDecorator:
    """Decorator to make class unique, so each time called same object returned
    """

    allInstances = []

    @staticmethod
    def destroyAll():
        for instance in SingletonDecorator.allInstances:
            instance.destroy()

    def __init__(self, cls):
        self.cls = cls
        self.instance = None
        self.allInstances.append(self)

    def destroy(self):
        if ('destroy' in dir(self.instance)):
            self.instance.destroy()
        del self.instance
        self.instance = None

    def __call__(self, *args, **kwds):
        if self.instance is None:
            self.instance = self.cls(*args, **kwds)
        if hasattr(self.instance, 'instanceCount'):
            self.instance.instanceCount += 1
        return self.instance


class DictElement(tuple):
    """PyFlow dict element class

    This subclass of python's :class:`tuple` is to represent dict elements to construct typed dicts
    """

    def __new__(cls, a=None, b=None):
        if a is None and b is None:
            new = ()
        elif b is None:
            if isinstance(a, tuple) and len(a) <= 2:
                new = a
            else:
                raise Exception("Invalid Input")
        else:
            new = (a, b)
        return super(DictElement, cls).__new__(cls, new)


class PFDict(dict):
    """This subclass of python's :class:`dict` implements a key typed dictionary.

    Only defined data types can be used as keys, and only hashable ones as determined by

    >>> isinstance(dataType, collections.abc.Hashable)

    To make a class Hashable some methods should be implemented:

    Example:
    ::

        class C:
            def __init__(self, x):
                self.x = x
            def __repr__(self):
                return "C({})".format(self.x)
            def __hash__(self):
                return hash(self.x)
            def __eq__(self, other):
                return (self.__class__ == other.__class__ and self.x == other.x)
    """

    def __init__(self, keyType, valueType="AnyPin", inp=None):
        """
        :param keyType: Key dataType
        :param valueType: value dataType, defaults to None
        :type valueType: optional
        :param inp: Construct from another dict, defaults to {}
        :type inp: dict, optional
        """
        if inp is None:
            inp = {}
        super(PFDict, self).__init__(inp)
        self.keyType = keyType
        self.valueType = valueType

    def __setitem__(self, key, item):
        """Re implements Python Dict __setitem__ to only allow Typed Keys.

        Will throw an Exception if non-Valid KeyType
        """
        if type(key) == self.getClassFromType(self.keyType):
            super(PFDict, self).__setitem__(key, item)
        else:
            raise Exception(
                "Valid key should be a {0}".format(self.getClassFromType(self.keyType))
            )

    @staticmethod
    def getClassFromType(pinType):
        """
        Gets the internal data structure for a defined pin type

        :param pinType: pinType Name
        :type pinType: class or None
        """
        pin = findPinClassByType(pinType)
        if pin:
            pinClass = pin.internalDataStructure()
            return pinClass
        return None


class PinReconnectionPolicy(IntEnum):
    """How to behave if pin has connections and another connection about to be performed.
    """

    DisconnectIfHasConnections = 0  #: Current connection will be broken
    ForbidConnection = 1  #: New connection will be cancelled


class PinOptions(Flag):
    """Used to determine how Pin behaves.

    Apply flags on pin instances.

    .. seealso:: :meth:`~PyFlow.Core.PinBase.PinBase.enableOptions` :meth:`~PyFlow.Core.PinBase.PinBase.disableOptions`
    """

    ArraySupported = auto()  #: Pin can hold array data structure
    DictSupported = auto()  #: Pin can hold dict data structure
    SupportsOnlyArrays = (
        auto()
    )  #: Pin will only support other pins with array data structure

    AllowMultipleConnections = (
        auto()
    )  #: This enables pin to allow more than one input connection. See :func:`~PyFlow.Core.Common.connectPins`

    ChangeTypeOnConnection = (
        auto()
    )  #: Used by :class:`~PyFlow.Packages.PyFlowBase.Pins.AnyPin.AnyPin` to determine if it can change its data type on new connection.
    RenamingEnabled = auto()  #: Determines if pin can be renamed
    Dynamic = (
        auto()
    )  #: Specifies if pin was created dynamically (during program runtime)
    AlwaysPushDirty = auto()  #: Pin will always be seen as dirty (computation needed)
    Storable = auto()  #: Determines if pin data can be stored when pin serialized
    AllowAny = (
        auto()
    )  #: Special flag that allow a pin to be :class:`~PyFlow.Packages.PyFlowBase.Pins.AnyPin.AnyPin`, which means non typed without been marked as error. By default, a :py:class:`PyFlow.Packages.PyFlowBase.Pins.AnyPin.AnyPin` need to be initialized with some data type, other defined pin. This flag overrides that. Used in lists and non typed nodes
    DictElementSupported = (
        auto()
    )  #: Dicts are constructed with :class:`DictElement` objects. So dict pins will only allow other dicts until this flag enabled. Used in :class:`~PyFlow.Packages.PyFlowBase.Nodes.makeDict` node


class StructureType(IntEnum):
    """Used to determine structure type for values.
    """

    Single = 0  #: Single data structure
    Array = (
        1
    )  #: Python list structure, represented as arrays -> typed and lists -> non typed
    Dict = 2  #: :py:class:`PFDict` structure, is basically a rey typed python dict
    Multi = (
        3
    )  #: This means it can become any of the previous ones on connection/user action


def findStructFromValue(value):
    """Finds :class:`~PyFlow.Core.Common.StructureType` from value

    :param value: input value to find structure.
    :returns: Structure Type for input value
    :rtype: :class:`~PyFlow.Core.Common.StructureType`
    """

    if isinstance(value, list):
        return StructureType.Array
    if isinstance(value, dict):
        return StructureType.Dict
    return StructureType.Single


class PinSelectionGroup(IntEnum):
    """Used in :meth:`~PyFlow.Core.NodeBase.NodeBase.getPinSG` for optimization purposes
    """

    Inputs = 0  #: Input pins
    Outputs = 1  #: Outputs pins
    BothSides = 2  #: Both sides pins


class AccessLevel(IntEnum):
    """Can be used for code generation
    """

    public = 0  #: public
    private = 1  #: private
    protected = 2  #: protected


class PinDirection(IntEnum):
    """Determines whether it is input pin or output
    """

    Input = 0  #: Left side pins
    Output = 1  #: Right side pins


class NodeTypes(IntEnum):
    """Determines whether it is callable node or pure
    """

    Callable = 0  #: Callable node is a node with exec pins
    Pure = 1  #: Normal nodes


class Direction(IntEnum):
    """ Direction identifiers
    """

    Left = 0  #: Left
    Right = 1  #: Right
    Up = 2  #: Up
    Down = 3  #: Down


class PinSpecifiers:
    """Pin specifiers constants

    :var SUPPORTED_DATA_TYPES: To specify supported data types list
    :var CONSTRAINT: To specify type constraint key
    :var STRUCT_CONSTRAINT: To specify struct constraint key
    :var ENABLED_OPTIONS: To enable options
    :var DISABLED_OPTIONS: To disable options
    :var INPUT_WIDGET_VARIANT: To specify widget variant string
    :var DESCRIPTION: To specify description for pin, which will be used as tooltip
    :var VALUE_LIST: Specific for string pin. If specified, combo box will be created
    :var VALUE_RANGE: Specific for ints and floats. If specified, slider will be created instead of value box
    :var DRAGGER_STEPS: To specify custom value dragger steps
    """

    SUPPORTED_DATA_TYPES = "supportedDataTypes"
    CONSTRAINT = "constraint"
    STRUCT_CONSTRAINT = "structConstraint"
    ENABLED_OPTIONS = "enabledOptions"
    DISABLED_OPTIONS = "disabledOptions"
    INPUT_WIDGET_VARIANT = "inputWidgetVariant"
    DESCRIPTION = "Description"
    VALUE_LIST = "ValueList"
    VALUE_RANGE = "ValueRange"
    DRAGGER_STEPS = "DraggerSteps"


class NodeMeta:
    """Node meta constants

    :var CATEGORY: To specify category for node. Will be considered by node box
    :var KEYWORDS: To specify list of additional keywords, used in node box search field
    :var CACHE_ENABLED: To specify if node is cached or not
    """

    CATEGORY = "Category"
    KEYWORDS = "Keywords"
    CACHE_ENABLED = "CacheEnabled"
